阅读指南
理论讲了这么多,该动手了。
这一节做一个真实的RAG应用——游戏知识问答助手。
《原神》玩家经常遇到各种问题:可莉的毕业武器是什么?胡桃的最优配装是什么?这个助手能从海量的游戏攻略、角色图鉴、玩家心得中找到最准确的答案。
做一个简易的《原神》角色知识库问答系统
数据资料
功能
回顾一下第2节讲的RAG流程:
准备数据 → 文档分块 → 向量化存储 → 用户提问 → 检索+生成
今天把这5步全部走一遍。
技术栈
两阶段设计
在正式开始之前,先理解一个重要的概念:RAG系统分为离线构建和在线查询两个阶段。
这两个词来自软件系统的设计模式:
在RAG系统中:
build_kb.py,提前把文档向量化存好,只运行一次query.py,每次用户提问时运行,负责检索和回答离线阶段(只运行一次)
- 读取原始文档
- 文档分块
- 向量化(调用Embedding API)
- 存储到向量数据库
输出:持久化的向量数据库
在线阶段(每次查询都运行)
- 加载已有的向量数据库
- 用户提问
- 检索相关文档
- 生成回答
输出:用户得到答案
如果每次用户提问都重新向量化所有文档,完全没有必要,因为文档内容没变,向量也不会变。离线程序只需成功执行一次,把文本向量化存入数据库即可。
项目文件结构
game-rag/
├── data/ # 原始数据
│ ├── 胡桃.txt
│ ├── 可莉.txt
│ ├── 钟离.txt
│ └── 甘雨.txt
├── build_kb.py # 离线:构建知识库(运行一次)
├── query.py # 在线:问答系统(每次查询运行)
└── chroma_db/ # 向量数据库(自动生成)
└── ...
Tip
完整项目代码参考:samples/chapter5/game-rag/
离线构建脚本负责读取文档、分块、向量化、存储。这个脚本只需要运行一次。
Tip
完整源码参考:samples/chapter5/game-rag/build_kb.py
按照RAG的流程,来看实现中的关键点。
API_KEY = os.getenv("DASHSCOPE_API_KEY", "sk-xxxxxxxxxxxx")
API_BASE = "https://dashscope.aliyuncs.com/compatible-mode/v1"
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(SCRIPT_DIR, "data")
DB_PATH = os.path.join(SCRIPT_DIR, "chroma_db")
os.getenv() 从环境变量读取API Key,避免硬编码泄露(强烈建议在生产环境使用这种方式)SCRIPT_DIR/DATA_DIR/DB_PATH 是目录和路径定义,不是API配置。它们属于脚本的"基础设施",后面会被复用到多个地方def split_document(file_path):
"""读取文档并按空行分块"""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
return [chunk.strip() for chunk in content.split('\n\n') if chunk.strip()]
split('\n\n') 利用数据文件的结构化特点if chunk.strip() 避免空内容进入数据库# 创建持久化客户端
client = chromadb.PersistentClient(path=DB_PATH)
qwen_ef = embedding_functions.OpenAIEmbeddingFunction(
api_key=API_KEY,
api_base=API_BASE,
model_name="text-embedding-v2"
)
collection = client.create_collection(
name="genshin_knowledge",
embedding_function=qwen_ef, # 注入Embedding函数
metadata={"hnsw:space": "cosine"} # 使用余弦相似度
)
PersistentClient(path=DB_PATH) 告诉Chroma数据存到磁盘上chroma_db目录,而不是临时内存。下次启动时可以重新加载qwen_efcollection.add() 时,Chroma会自动调用 qwen_ef 进行向量化"hnsw:space": "cosine" 适合文本检索场景all_chunks, all_metadata, all_ids = [], [], []
txt_files = [f for f in os.listdir(DATA_DIR) if f.endswith('.txt')]
for filename in txt_files:
character_name = filename.replace('.txt', '')
file_path = os.path.join(DATA_DIR, filename)
chunks = split_document(file_path)
# 批量构建当前文件的所有数据
all_chunks.extend(chunks)
all_metadata.extend([
{"character": character_name, "source": filename, "chunk_id": i}
for i in range(len(chunks))
])
all_ids.extend([f"{character_name}_{i}" for i in range(len(chunks))])
character、source、chunk_id 三个字段,方便检索后追溯f"{character_name}_{i}" 确保每个文档块都有唯一标识extend() 配合列表推导,代码简洁batch_size = 25 # Qwen API限制每批最多25个文本
for i in range(0, len(all_chunks), batch_size):
collection.add(
documents=all_chunks[i:i+batch_size],
metadatas=all_metadata[i:i+batch_size],
ids=all_ids[i:i+batch_size]
)
qwen_ef 进行向量化,不需要手动调用$ python build_kb.py
开始构建知识库...
处理文件: 钟离.txt
处理文件: 胡桃.txt
处理文件: 可莉.txt
处理文件: 甘雨.txt
正在向量化 44 个文档块...
处理第 1 批(25 个文档块)...
处理第 2 批(19 个文档块)...
知识库构建完成!
数据库位置: ./chroma_db
总文档块数: 44
角色数量: 4
在线查询脚本负责加载知识库、检索、生成回答。这个脚本每次用户查询时运行。
Tip
完整源码参考:samples/chapter5/game-rag/query.py
继续按照RAG的流程,来看实现中的关键点。
def load_knowledge_base():
"""加载已有的向量知识库"""
client = chromadb.PersistentClient(path=DB_PATH)
# 配置Embedding(必须与构建时一致)
qwen_ef = embedding_functions.OpenAIEmbeddingFunction(
api_key=API_KEY,
api_base=API_BASE,
model_name="text-embedding-v2"
)
# 获取集合(不是create,而是get)
collection = client.get_collection(
name="genshin_knowledge",
embedding_function=qwen_ef
)
return collection
get_collection() 而不是 create_collection(),加载已有数据def ask_question(collection, query):
"""基于RAG回答问题"""
# 检索相关文档
results = collection.query(
query_texts=[query],
n_results=3
)
retrieved_docs = results['documents'][0]
# 构造上下文
context = "\n\n".join([
f"【参考资料{i+1}】\n{doc}"
for i, doc in enumerate(retrieved_docs)
])
qwen_ef 把问题向量化n_results=3 返回最相关的3个文档块prompt = f"""你是一个《原神》游戏助手,请基于以下参考资料回答用户的问题。
参考资料:
{context}
用户问题:{query}
回答要求:
1. 基于参考资料回答,不要编造信息
2. 回答要专业但不失亲和力
3. 如果有多个选择,要说明优先级
4. 可以适当补充游戏常识
"""
client = OpenAI(api_key=API_KEY, base_url=API_BASE)
response = client.chat.completions.create(
model="qwen3.6-plus",
messages=[{"role": "user", "content": prompt}],
temperature=0.7
)
return response.choices[0].message.content
def main():
# 加载知识库(只加载一次)
collection = load_knowledge_base()
# 交互式问答
while True:
query = input("你:").strip()
if query in ['退出', 'quit', 'exit']:
break
if not query:
continue
answer = ask_question(collection, query)
print(f"\n助手:{answer}\n")
if not query: continue 避免空查询$ python query.py
==================================================
《原神》知识问答助手
==================================================
正在加载知识库...
知识库加载完成
开始对话(输入'退出'结束)
你:胡桃用什么武器最好?
助手:胡桃的最佳武器推荐如下:
毕业武器(首选)
**护摩之杖**(五星)
- 基础攻击:608
- 副属性:暴击伤害66.2%
- 特效:生命值转化攻击力,完美契合胡桃机制
替代选择
**龙吟**(五星)适合暴伤型配装
**决斗之枪**(四星)平民神器,性价比高
建议:有护摩优先护摩,没有可以用龙吟或决斗之枪。
问:明天会下雨吗?
答:哎呀,旅行者,关于"明天会不会下雨"这个问题,我得老实告诉你——我的参考资料里可没有天气预报功能哦!😅。不过既然你提到了"雨",咱们倒是可以聊聊《原神》里的角色配队!比如在甘雨的冻结队中,行秋或莫娜就能提供稳定的水元素附着,配合甘雨的冰箭打出"冻结"反应,
到这里,已经用不到200行代码实现了一个能跑的RAG应用。但现实中的文档不会这么规整——100页的PDF、扫描版、双栏排版、表格跨页……该怎么切?切得太大,检索不精准;切得太小,语义不完整。下一节来看文档分块这门艺术,看看它是如何决定RAG系统的生死。
| 中文 | English | 音标 | 说明 |
|---|---|---|---|
| 知识库 | Knowledge Base | /ˈnɑːlɪdʒ beɪs/ | 存储结构化/非结构化文档的系统,RAG检索的数据来源 |
| 元数据 | Metadata | /ˈmetədeɪtə/ | 描述数据的数据,如文档来源、类型、标签、时间等 |
| 语义相关度 | Semantic Relevance | /sɪˈmæntɪk ˈreləvəns/ | 检索结果与用户查询之间的语义匹配程度 |
| 上下文质量 | Context Quality | /ˈkɑːntekst ˈkwɑːləti/ | 传给AI的检索结果的相关性和完整性度量 |